---
title: "8：LBの向上"
---

## 問題点

[前のパート](/tutorial/07-load-balancing)では、うまく動作するロードバランサーを作成できましたが、いくつかの問題があります。1つの接続で2つの連続したリクエストを送信してみましょう。

``` sh
$ curl localhost:8000/hi localhost:8000/hi
Hi, there!
You are requesting /hi from ::ffff:127.0.0.1
```

ロードバランサーとしては間違った振舞いではありませんが、最適ではないことは確かです。通常、同じ接続から来たリクエストは、同じターゲットに送られて欲しいわけです。

## Cache

この「_同じ接続からのリクエストは同じターゲットに向かう_」というルールに従うロジックは、単純な「_cache_」で実現できます。接続内のすべてのリクエストに対して、常に[_RoundRobinLoadBalancer.prototype.select()_](/reference/api/algo/RoundRobinLoadBalancer/select)をコールするのではなく、次のようにします。

1.	キャッシュを検索して、バランサーがすでにターゲットを選択しているかどうかを見る
2.	見つかった場合、すでに選択されたターゲットを使う
3.	見つからなかった場合、`select()`をコールして1つ選択し、次の使用のためにキャッシュに保持する

このキャッシュは[_algo.Cache_](/reference/api/algo/Cache)を使って作ります。これには2つのコールバックがあります。

1.	不足しているエントリーが埋められたときのコールバック
2.	エントリーが消去されたときのコールバック

ここでは、これらのコールバックの中で`RoundRobinLoadBalancer`をコールします。

``` js
new algo.Cache(
  // k is a balancer, v is a target
  (k  ) => k.select(),
  (k,v) => k.deselect(v),
)
```

ここで、 _RoundRobinLoadBalancer_ オブジェクトをキーに使っていることに注意してください。

> [_RoundRobinLoadBalancer.prototype.deselect()_](/reference/api/algo/RoundRobinLoadBalancer/deselect)のコールは必ずしも必要ではありませんが、[_LeastWorkLoadBalancer_](/reference/api/algo/LeastWorkLoadBalancer)のような他のタイプのロードバランスアルゴリズムの場合、ワークロードを追跡できるようにするために必要になります。 

### 初期化

バランサーからターゲットへのキャッシュは、着信接続がある都度、一度だけ作成されるべきなので、HTTPセッションの一番初めに初期化します。プラグインにはまだそのポイントがないので、メインのスクリプト`/proxy.js`の`listen()`の直後、ストリームが入ってきたところに挿入し、 _use()_ を使ってプラグインのすべての「_session_」サブパイプラインにチェーンさせます。

``` js
  pipy()

  .export('proxy', {
    __turnDown: false,
  })

  .listen(config.listen)
+   .use(config.plugins, 'session')
    .demuxHTTP('request')

  .pipeline('request')
    .use(
      config.plugins,
      'request',
      'response',
      () => __turnDown
    )
```

初期化は、`/plugins/balancer.js`内の新しい「_session_」サブパイプラインで行うことができます。

``` js
  pipy({
    _services: (
      Object.fromEntries(
        Object.entries(config.services).map(
          ([k, v]) => [
            k, new algo.RoundRobinLoadBalancer(v)
          ]
        )
      )
    ),

+   _balancer: null,
+   _balancerCache: null,
    _target: '',
  })

  .import({
    __turnDown: 'proxy',
    __serviceID: 'router',
  })

+ .pipeline('session')
+   .handleStreamStart(
+     () => (
+       _balancerCache = new algo.Cache(
+         // k is a balancer, v is a target
+         (k  ) => k.select(),
+         (k,v) => k.deselect(v),
+       )
+     )
+   )
```

また、次で使う`_balancer`という変数を定義しておきます。 

### キャッシュする

ここでは、 _RoundRobinLoadBalancer.select()_ を直接コールするのではなく、 _RoundRobinLoadBalancer.select()_ へのコールをラップする[_algo.Cache.prototype.get()_](/reference/api/algo/Cache/get)をコールします。

``` js
  .pipeline('request')
    .handleMessageStart(
      () => (
-       _target = _services[__serviceID]?.select?.(),
+       _balancer = _services[__serviceID],
+       _balancer && (_target = _balancerCache.get(_balancer)),
        _target && (__turnDown = true)
      )
    )
    .link(
      'forward', () => Boolean(_target),
      ''
    )
```

### クリーンアップ

最後に、受信接続が終了するとき、キャッシュをクリアします。

``` js
  .pipeline('session')
    .handleStreamStart(
      () => (
        _balancerCache = new algo.Cache(
          // k is a balancer, v is a target
          (k  ) => k.select(),
          (k,v) => k.deselect(v),
        )
      )
    )
+   .handleStreamEnd(
+     () => (
+       _balancerCache.clear()
+     )
+   )
```

## テストしてみる

最後に、このパートの冒頭と同じテストをすると、次のようになります。

``` sh
$ curl localhost:8000/hi localhost:8000/hi
Hi, there!
Hi, there!
$ curl localhost:8000/hi localhost:8000/hi
You are requesting /hi from ::ffff:127.0.0.1
You are requesting /hi from ::ffff:127.0.0.1
```

_curl_ で接続するたびに、2つのリクエストは同じターゲットを取得します。異なる接続間でのみ、ターゲットはローテーションしています。

## まとめ

このパートでは、同じサービスに向かうリクエストに対して、特定の接続の場合、同じターゲットに留まるようにする方法を解説しました。これは、典型的なロードバランシングプロキシの最適化です。

### 要点

1.	[_algo.Cache_](/reference/api/algo/Cache)を使用すると、サービスに割り当てられたターゲットを記憶し、同じ接続に対して同じターゲットに戻ることができます。
2.	接続全体の初期化は、ストリームが _demuxHTTP()_ のようなフィルタでメッセージに分割される前に、 _listen()_ の最初で行われるようにします。

### 次のパートの内容

もう一つ、このロードバランサーで最適化できるものがあります。それはコネクションプールです。次のパートではこれを解説します。

